DLO-JZ WebDataset, Data Augmentation - Jour 3¶

car

Objet du notebook¶

Le but de ce notebook est d'optimiser la DataLoader afin de ne pas ralentir la boucle d'apprentissage. L'étude de la performance des solutions optimisées se fera en visualisant les traces du profiler :

  • TP 1 : Optimisation du DataLoader au format Webdataset
  • TP 2 : Data Augmentation

Les cellules dans ce notebook ne sont pas prévues pour être modifiées, sauf rares exceptions indiquées dans les commentaires. Les TP se feront en modifiant le code dlojz.py.

Les directives de modification seront marquées par l'étiquette TODO : dans le notebook suivant.

Les solutions sont présentes dans le répertoire solutions.

Notebook rédigé par l'équipe assistance IA de l'IDRIS, juin 2023


Environnement de calcul¶

Un module PyTorch doit avoir été chargé pour le bon fonctionnement de ce Notebook. Nécessairement, le module pytorch-gpu/py3/1.11.0 :

In [1]:
!module list
Currently Loaded Modulefiles:
 1) cuda/11.2                5) openmpi/4.1.1-cuda   9) sparsehash/2.0.3        
 2) nccl/2.9.6-1-cuda        6) intel-mkl/2020.4    10) libjpeg-turbo/2.1.3     
 3) cudnn/8.1.1.33-cuda      7) magma/2.5.4-cuda    11) pytorch-gpu/py3/1.11.0  
 4) gcc/8.5.0(8.3.1:8.4.1)   8) sox/14.4.2          
>

Les fonctions python de gestion de queue SLURM dévelopées par l'IDRIS et les fonctions dédiées à la formation DLO-JZ sont à importer.

Le module d'environnement pour les jobs et la taille des images sont fixés pour ce notebook.

TODO : choisir un pseudonyme (maximum 5 caractères) pour vous différencier dans la queue SLURM et dans les outils collaboratifs pendant la formation et la compétition.

In [2]:
from idr_pytools import display_slurm_queue, gpu_jobs_submitter, search_log
from dlojz_tools import controle_technique, compare, GPU_underthehood, plot_accuracy, lrfind_plot, imagenet_starter, turbo_profiler
MODULE = 'pytorch-gpu/py3/1.11.0'
account = 'for@v100'
name = 'pseudo'   ## Pseudonyme à choisir

Gestion de la queue SLURM¶

Cette partie permet d'afficher et de gérer la queue SLURM.

Pour afficher toute la queue utilisateur :

In [3]:
display_slurm_queue(name)
 Done!

Remarque: Cette fonction utilisée plusieurs fois dans ce notebook permet d'afficher la queue de manière dynamique, rafraichie toutes les 5 secondes. Cependant elle ne s'arrête que lorsque la queue est vide. Si vous désirez reprendre la main sur le notebook, il vous suffira d'arrêter manuellement la cellule avec le bouton stop. Cela a bien sûr aucun impact sur le scheduler SLURM. Les jobs ne seront pas arrêtés.

Si vous voulez annuler un job dans votre queue, décommenter la ligne suivante et remplacer le numéro du job.

In [4]:
#!scancel 2088207

Debug¶

Cette partie debug permet d'afficher les fichiers de sortie et les fichiers d'erreur du job.

Il est nécessaire dans la cellule suivante d'indiquer le jobid correspondant sous le format donné.

*Remarque* : dans ce notebook, lorsque vous soumettrez un job, vous recevrez en retour le numéro du job dans le format suivant : jobid = ['123456']. La cellule ci-dessous peut ainsi être facilement actualisée.

In [5]:
#jobid = ['2088207']

Fichier de sortie :

In [6]:
%cat {search_log(contains=jobid[0])[0]}
/bin/bash: -c: line 0: syntax error near unexpected token `('
/bin/bash: -c: line 0: `cat {search_log(contains=jobid[0])[0]}'

Fichier d'erreur :

In [7]:
%cat {search_log(contains=jobid[0], with_err=True)['stderr'][0]}
/bin/bash: -c: line 0: syntax error near unexpected token `('
/bin/bash: -c: line 0: `cat {search_log(contains=jobid[0], with_err=True)['stderr'][0]}'

Différence entre deux scripts¶

Pour le debug ou pour comparer son code avec les solutions mises à disposition, la fonction suivante permet d'afficher une page html contenant un différentiel de fichiers texte.

In [8]:
s1 = "dlojz.py"
s2 = "./solutions/dlojz3_1.py"
compare(s1, s2)

Voir le résultat du différentiel de fichiers sur la page suivante (attention au spoil !) :

compare.html


Garage - Mise à niveau¶

On fixe le batch size et la taille d'image pour ce TP.

In [9]:
bs_optim = 512
image_size = 176

TODO : Comparer votre script dlojz.py avec ce qu'il devrait être actuellement. Si il y a des divergences, veuillez les corriger (par exemple en copiant-collant la solution).

In [10]:
s1 = "dlojz.py"
s2 = "./solutions/dlojz3_1.py"
compare(s1, s2)

Voir le résultat du différentiel de fichiers sur la page suivante :

compare.html

In [11]:
# copier/coller la solution si nécessaire
#!cp solutions/dlojz3_0.py dlojz.py

TP3_1 : Optimisation du DataLoader - Format WebDataset¶

Le but de ce TP est d'utiliser un IterableDataset sur des données d'entrée au format WebDataset et de le comparer avec le Dataset Map-style de torchvision précédemment vu.

Implémentation du format WebDataset¶

TODO : dans le script dlojz.py :

  • Importer la librairie webdataset.
import webdataset as wds
  • Remplacer l'implémentation du train_dataset, du train_loader et du train_sampler par l'implémentation suivante.
train_dataset = (
        wds.WebDataset(os.environ['ALL_CCFRSCRATCH']+'/imagenet/webdataset/imagenet_train-{000000..000127}.tar', shardshuffle=True, nodesplitter=wds.split_by_node)
        .shuffle(1000)
        .decode("torchrgb")
        .to_tuple('input.pyd', 'output.pyd')
        .map_tuple(transform, lambda x: x)
        .batched(mini_batch_size)
        )

    dataset_size = 1281167
    number_of_batches = dataset_size // global_batch_size
    train_loader = wds.WebLoader(train_dataset,
                                 batch_size=None,
                                 num_workers=args.num_workers,
                                 persistent_workers=args.persistent_workers,
                                 pin_memory=args.pin_memory,
                                 prefetch_factor=args.prefetch_factor,
                                 drop_last=args.drop_last)

    train_loader = train_loader.slice(number_of_batches)
    train_loader.length = number_of_batches
  • Puisqu'il n'y a plus de train_sampler (la distribution des batches sur les différents workers se fait avec le paramètre nodesplitter=wds.split_by_node), effacer ou commenter la ligne suivante :
#train_sampler.set_epoch(epoch)
  • Un dataset de type IterableDataset ne connaissant pas sa longueur, la longueur du loader est définie par train_loader.length = number_of_batches. Modifier la déclaration de la variable N_batch en conséquence :
N_batch = train_loader.length

Contrôle technique (version sous-optimisée)¶

TODO : lancer l'exécution sur 50 itérations (--test-nsteps 50) sans profiling pour passer un contrôle technique qui servira de référence. Cette exécution va prendre quelques minutes, vous pouvez passer à la suite du TP sans attendre la fin de l'exécution.

Soumission du job. Attention vous sollicitez les noeuds de calcul à ce moment-là.

Pour soumettre le job, veuillez basculer la cellule suivante du mode Raw NBConvert au mode Code.

In [12]:
command = f'dlojz.py -b {bs_optim} --image-size {image_size} --test --test-nsteps 50'
command += f' --num-workers 0 --no-persistent-workers --no-pin-memory --no-non-blocking --prefetch-factor 2'
n_gpu = 1
jobid = gpu_jobs_submitter(command, n_gpu, MODULE, name=name,
                    account=account, time_max='00:10:00', constraint='v100-32g')
print(f'jobid = {jobid}')
batch job 0: 1 GPUs distributed on 1 nodes with 1 tasks / 1 gpus per node and 10 cpus per task
Submitted batch job 248210
jobid = ['248210']
In [13]:
display_slurm_queue(name)
             JOBID PARTITION     NAME     USER ST       TIME  NODES NODELIST(REASON)
            248210   gpu_p13   pseudo  cfor132  R       3:08      1 r6i3n0

 Done!
In [14]:
#jobid = ['1587014']
In [15]:
controle_technique(jobid)
Train throughput: 268.87 images/second
GPU throughput: 1783.88 images/second
epoch time: 4764.49 seconds
training time estimation for 90 epochs (with validations): 128.44 hours
-----------
training step time average (fwd/bkwd on GPU): 0.287015 sec (39.1%/60.6%) +/- 0.001335
loading step time average (CPU to GPU): 1.617259 sec +/- 0.087032
-----------
ELIGIBLE to run 11 epochs
In [16]:
turbo_profiler(jobid)
>>> Turbo Profiler >>> Training complete in 141.384106 s

Visualisation des traces profiler Tensorboard (version sous-optimisée)¶

TODO : étudier les traces du cas sous-optimisé "num_workers=0" afin de mesurer l'accélération brute de ce type de Dataset.

Soumission du job. Attention vous sollicitez les noeuds de calcul à ce moment-là.

Pour soumettre le job, veuillez basculer la cellule suivante du mode Raw NBConvert au mode Code.

In [17]:
command = f'dlojz.py -b {bs_optim} --image-size {image_size} --test --test-nsteps 15 --prof'
command += f' --num-workers 0 --no-persistent-workers --no-pin-memory --no-non-blocking --prefetch-factor 2'
n_gpu = 1
jobid = gpu_jobs_submitter(command, n_gpu, MODULE, name=name,
                    account=account, time_max='00:10:00', constraint='v100-32g')
print(f'jobid = {jobid}')
batch job 0: 1 GPUs distributed on 1 nodes with 1 tasks / 1 gpus per node and 10 cpus per task
Submitted batch job 248218
jobid = ['248218']

Puis, rebasculer la cellule précédente en mode Raw NBConvert, afin d'eviter de relancer un job par erreur.

In [18]:
display_slurm_queue(name)
             JOBID PARTITION     NAME     USER ST       TIME  NODES NODELIST(REASON)
            248218   gpu_p13   pseudo  cfor132  R       1:59      1 r6i3n0

 Done!
In [19]:
#jobid = ['1587676']

TODO : vérifier qu'une trace a bien été générée dans le répertoire profiler/<name>_<jobid>_bs512_is176/ sous la forme d'un fichier .json:

In [20]:
!ls profiler/{name}_{jobid[0]}*
r6i3n0_632542.1687733989173.pt.trace.json

TODO : visualiser cette trace grâce à l'application TensorBoard (retrouver la procédure) et comparer les traces obtenues avec le dataset torchvision et le dataset webdataset.

IMPORTANT : une fois le TP terminé, penser à quitter l'instance JupyterHub pour libérer le GPU ( > Hub Control Panel > Cancel ).

Exploration des paramètres d'optimisation du DataLoader¶

Ensuite, l'objectif de ce TP est de réduire le temps passé sur CPU par le DataLoader WebDataset.

Les différentes optimisations proposées par le DataLoader sont accessibles dans le script dlojz.py via les arguments :

  • --num-workers <num_workers> (défaut à 10)
  • --persistent-workers (défaut) ou --no-persistent-workers
  • --pin-memory (défaut) ou --no-pin-memory
  • --non-blocking (défaut) ou --no-non-blocking
  • --prefetch-factor <prefetch_factor> (défaut à 3)
  • --drop-last ou --no-drop-last (défaut)

TODO : faire varier ces différents paramètres et observer leurs effets grâce au profiler turbo_profiler

Remarque : pour cette étude, on ne lance les exécutions que sur 15 itérations (--test-nsteps 15) pour avancer plus rapidement.

Les différents essais seront stockés dans une DataFrame dataloader_trials :

In [21]:
import pandas as pd
dataloader_trials = pd.DataFrame({"jobid":pd.Series([],dtype=str),
                                  "num_workers":pd.Series([],dtype=int),
                                  "persistent_workers":pd.Series([],dtype=str),
                                  "pin_memory":pd.Series([],dtype=str),
                                  "non_blocking":pd.Series([],dtype=str),
                                  "prefetch_factor":pd.Series([],dtype=int),
                                  "drop_last":pd.Series([],dtype=str),
                                  "loading_time":pd.Series([],dtype=float),
                                  "training_time":pd.Series([],dtype=float)})

Soumission du job. Attention vous sollicitez les noeuds de calcul à ce moment-là.

Pour soumettre le job, veuillez basculer la cellule suivante du mode Raw NBConvert au mode Code.

In [22]:
command = f'dlojz.py -b {bs_optim} --image-size {image_size} --test --test-nsteps 15'

# paramètres d'entrée correspondant aux optimisations du DataLoader
command += ' --num-workers 8' 
command += ' --persistent-workers'
command += ' --pin-memory'
command += ' --non-blocking'
command += ' --prefetch-factor 2'
command += ' --no-drop-last'

n_gpu = 1
jobid = gpu_jobs_submitter(command, n_gpu, MODULE, name=name,
                    account=account, time_max='00:10:00', constraint='v100-32g')
print(f'jobid = {jobid}')
batch job 0: 1 GPUs distributed on 1 nodes with 1 tasks / 1 gpus per node and 10 cpus per task
Submitted batch job 248223
jobid = ['248223']
In [23]:
display_slurm_queue(name)
             JOBID PARTITION     NAME     USER ST       TIME  NODES NODELIST(REASON)
            248223   gpu_p13   pseudo  cfor132  R       0:29      1 r6i3n0

 Done!
In [26]:
jobid = ['248223']
In [27]:
# call turbo_profiler
dataloader_trial = turbo_profiler(jobid,dataloader_info=True)
# store result in "dataloader_trials" DataFrame
dataloader_trials = pd.concat([dataloader_trials,dataloader_trial], ignore_index=True)
>>> Turbo Profiler >>> Training complete in 19.322983 s
In [28]:
# afficher le tableau récapitulatif, trier par ordre croissant du LOADING_TIME
dataloader_trials.sort_values("loading_time")
Out[28]:
jobid num_workers persistent_workers pin_memory non_blocking prefetch_factor drop_last loading_time training_time
0 248223 8 True True True 2 False 0.000474 19.322983
In [29]:
# afficher le tableau récapitulatif, trier par ordre croissant du TRAINING_TIME
dataloader_trials.sort_values("training_time")
Out[29]:
jobid num_workers persistent_workers pin_memory non_blocking prefetch_factor drop_last loading_time training_time
0 248223 8 True True True 2 False 0.000474 19.322983

Visualisation des traces profiler avec TensorBoard (version optimisée)¶

TODO : après avoir choisi un lot de paramètres optimal, relancer le job en réactivant le profiler PyTorch (argument d'entrée --prof) afin de visualiser les traces sous TensorBoard.

Soumission du job. Attention vous sollicitez les noeuds de calcul à ce moment-là.

Pour soumettre le job, veuillez basculer la cellule suivante du mode Raw NBConvert au mode Code.

In [30]:
command = f'dlojz.py -b {bs_optim} --image-size {image_size} --test --prof --test-nsteps 15'

# définir ici les paramètres optimaux 
command += ' --num-workers 8' 
command += ' --persistent-workers'
command += ' --pin-memory'
command += ' --non-blocking'
command += ' --prefetch-factor 2'
command += ' --no-drop-last'

n_gpu = 1
jobid = gpu_jobs_submitter(command, n_gpu, MODULE, name=name,
                    account=account, time_max='00:10:00', constraint='v100-32g')
print(f'jobid = {jobid}')
batch job 0: 1 GPUs distributed on 1 nodes with 1 tasks / 1 gpus per node and 10 cpus per task
Submitted batch job 248226
jobid = ['248226']

Puis, rebasculer la cellule précédente en mode Raw NBConvert, afin d'éviter de relancer un job par erreur.

In [31]:
display_slurm_queue(name)
             JOBID PARTITION     NAME     USER ST       TIME  NODES NODELIST(REASON)
            248226   gpu_p13   pseudo  cfor132  R       0:37      1 r6i3n0

 Done!

TODO : vérifier qu'une trace a bien été générée dans le répertoire profiler/<name>_<jobid>_bs512_is176/ sous la forme d'un fichier .json:

In [32]:
!ls profiler/{name}_{jobid[0]}*
r6i3n0_633725.1687734182850.pt.trace.json

TODO : visualiser cette trace grâce à l'application TensorBoard (retrouver la procédure).

IMPORTANT : une fois le TP terminé, penser à quitter l'instance JupyterHub pour libérer le GPU ( > Hub Control Panel > Cancel ).

Contrôle technique (version optimisée)¶

TODO : lancer l'exécution sur 50 itérations (--test-nsteps 50) sans profiling pour passer un nouveau contrôle technique, à comparer avec celui de référence.

Soumission du job. Attention vous sollicitez les noeuds de calcul à ce moment-là.

Pour soumettre le job, veuillez basculer la cellule suivante du mode Raw NBConvert au mode Code.

In [33]:
command = f'dlojz.py -b {bs_optim} --image-size {image_size} --test --test-nsteps 50'

# définir ici les paramètres optimaux 
command += ' --num-workers 8' 
command += ' --persistent-workers'
command += ' --pin-memory'
command += ' --non-blocking'
command += ' --prefetch-factor 2'
command += ' --no-drop-last'

n_gpu = 1
jobid = gpu_jobs_submitter(command, n_gpu, MODULE, name=name,
                    account=account, time_max='00:10:00', constraint='v100-32g')
print(f'jobid = {jobid}')
batch job 0: 1 GPUs distributed on 1 nodes with 1 tasks / 1 gpus per node and 10 cpus per task
Submitted batch job 248228
jobid = ['248228']
In [34]:
display_slurm_queue(name)
             JOBID PARTITION     NAME     USER ST       TIME  NODES NODELIST(REASON)
            248228   gpu_p13   pseudo  cfor132 CG       0:43      1 r6i3n0

 Done!
In [35]:
#jobid = ['1587014']
In [36]:
controle_technique(jobid)
Train throughput: 1702.20 images/second
GPU throughput: 1703.77 images/second
epoch time: 752.57 seconds
training time estimation for 90 epochs (with validations): 20.53 hours
-----------
training step time average (fwd/bkwd on GPU): 0.300510 sec (41.5%/58.3%) +/- 0.001797
loading step time average (CPU to GPU): 0.000278 sec +/- 0.000034
-----------
ELIGIBLE to run 41 epochs
In [37]:
turbo_profiler(jobid)
>>> Turbo Profiler >>> Training complete in 29.88395 s

Garage

TP3_2 : Data Augmentation¶

TP3_2_0 : RandAugment¶

Le but de ce TP est d'ajouter la transformation RandAugment (disponible dans torchvision) dans la liste des transformations pour la Data Augmentation et de mesurer grâce au profiler ce que cela implique pour le DataLoader.

Il faut repartir d'un scriptdlojz.py propre :

In [38]:
# copier/coller la solution si nécessaire
!cp solutions/dlojz3_0.py dlojz.py
In [39]:
import os
import torchvision
import torchvision.transforms as transforms
import torchvision.models as models
import torch
import numpy as np
import matplotlib.pyplot as plt

transform = transforms.Compose([ 
        transforms.RandomResizedCrop(176),  # Random resize - Data Augmentation
        transforms.RandomHorizontalFlip(),  # Horizontal Flip - Data Augmentation
        transforms.RandAugment(5, 9),       # Random Augmentation 2: n operations, 9 : magnitude 
        transforms.ToTensor()               # convert the PIL Image to a tensor
        ])
    
    
train_dataset = torchvision.datasets.ImageNet(root=os.environ['ALL_CCFRSCRATCH']+'/imagenet',
                                                  transform=transform)
train_dataset
Out[39]:
Dataset ImageNet
    Number of datapoints: 1281167
    Root location: /gpfsscratch/idris/for/commun/imagenet
    Split: train
    StandardTransform
Transform: Compose(
               RandomResizedCrop(size=(176, 176), scale=(0.08, 1.0), ratio=(0.75, 1.3333), interpolation=bilinear)
               RandomHorizontalFlip(p=0.5)
               RandAugment(num_ops=5, magnitude=9, num_magnitude_bins=31, interpolation=InterpolationMode.NEAREST, fill=None)
               ToTensor()
           )
In [40]:
%%time

train_loader = torch.utils.data.DataLoader(dataset=train_dataset,    
                                           batch_size=4,
                                           shuffle=True)
batch = next(iter(train_loader))
print('X train batch, shape: {}, data type: {}, Memory usage: {} bytes'
      .format(batch[0].shape, batch[0].dtype, batch[0].element_size()*batch[0].nelement()))
print('Y train batch, shape: {}, data type: {}, Memory usage: {} bytes'
      .format(batch[1].shape, batch[1].dtype, batch[1].element_size()*batch[1].nelement()))

for i in range(4):
    img = batch[0][i].numpy().transpose((1,2,0))
    plt.imshow(img)
    plt.axis('off')
    plt.show()
X train batch, shape: torch.Size([4, 3, 176, 176]), data type: torch.float32, Memory usage: 1486848 bytes
Y train batch, shape: torch.Size([4]), data type: torch.int64, Memory usage: 32 bytes
CPU times: user 2.15 s, sys: 389 ms, total: 2.54 s
Wall time: 2.69 s

TODO : dans le script dlojz.py :

  • Rajouter la transformation RandAugmentdans la liste des transformations pour la Data Augmentation
transform = transforms.Compose([ 
        transforms.RandomResizedCrop(args.image_size),  # Random resize - Data Augmentation
        transforms.RandomHorizontalFlip(),              # Horizontal Flip - Data Augmentation
        transforms.RandAugment(2, 9),                   # Random Augmentation 2:n operations, 9:magnitude 
        transforms.ToTensor(),                          # convert the PIL Image to a tensor
        transforms.Normalize(mean=(0.485, 0.456, 0.406),
                             std=(0.229, 0.224, 0.225))
        ])
In [41]:
command = f'dlojz.py -b {bs_optim} --image-size {image_size} --test'
command
Out[41]:
'dlojz.py -b 512 --image-size 176 --test'

Soumission du job. Attention vous sollicitez les noeuds de calcul à ce moment-là.

Pour soumettre le job, veuillez basculer la cellule suivante du mode Raw NBConvert au mode Code.

In [42]:
command = f'dlojz.py -b {bs_optim} --image-size {image_size} --test'
n_gpu = 1
jobid = gpu_jobs_submitter(command, n_gpu, MODULE, name=name,
                    account=account, time_max='00:10:00', constraint='v100-32g')
print(f'jobid = {jobid}')
batch job 0: 1 GPUs distributed on 1 nodes with 1 tasks / 1 gpus per node and 10 cpus per task
Submitted batch job 248262
jobid = ['248262']

Copier-coller la sortie jobid = ['xxxxx'] dans la cellule suivante.

Puis, rebasculer la cellule précédente en mode Raw NBConvert, afin d'éviter de relancer un job par erreur.

In [43]:
#jobid = ['1588551']
In [44]:
display_slurm_queue(name)
             JOBID PARTITION     NAME     USER ST       TIME  NODES NODELIST(REASON)
            248262   gpu_p13   pseudo  cfor132  R       0:55      1 r6i3n0

 Done!
In [45]:
controle_technique(jobid)
Train throughput: 1701.43 images/second
GPU throughput: 1704.01 images/second
epoch time: 753.21 seconds
training time estimation for 90 epochs (with validations): 21.16 hours
-----------
training step time average (fwd/bkwd on GPU): 0.300468 sec (43.7%/62.8%) +/- 0.058110
loading step time average (CPU to GPU): 0.000455 sec +/- 0.000671
-----------
ELIGIBLE to run 41 epochs
In [46]:
turbo_profiler(jobid)
>>> Turbo Profiler >>> Training complete in 34.876351 s

Commentaires


TP3_2_1 : Mixup¶

Le but de ce TP est d'ajouter la transformation Mixup dans la liste des transformations pour la Data Augmentation et de mesurer grâce au profiler ce que cela implique pour le DataLoader.

La transformation MixUp n'est pas disponible dans torchvision, le script est disponible dans le répertoire mixup/. On notera que cette transformation impacte à la fois l'image et le label.

On choisira, comme cela est fait habituellement, de mixer 2 images présentes dans le batch généré par le DataLoader. Donc cette transformation sera faite dans la boucle d'apprentissage après génération du batch et après toutes autres transformations liées à la Data Augmentation.

In [47]:
import os
import torchvision
import torchvision.transforms as transforms
import torchvision.models as models
import torch
import numpy as np
import matplotlib.pyplot as plt

transform = transforms.Compose([ 
        transforms.RandomResizedCrop(176),  # Random resize - Data Augmentation
        transforms.RandomHorizontalFlip(),  # Horizontal Flip - Data Augmentation
        transforms.ToTensor()               # convert the PIL Image to a tensor
        ])
    
    
train_dataset = torchvision.datasets.ImageNet(root=os.environ['ALL_CCFRSCRATCH']+'/imagenet',
                                                  transform=transform)
train_dataset
Out[47]:
Dataset ImageNet
    Number of datapoints: 1281167
    Root location: /gpfsscratch/idris/for/commun/imagenet
    Split: train
    StandardTransform
Transform: Compose(
               RandomResizedCrop(size=(176, 176), scale=(0.08, 1.0), ratio=(0.75, 1.3333), interpolation=bilinear)
               RandomHorizontalFlip(p=0.5)
               ToTensor()
           )
In [48]:
from mixup.mixup import mixup_data
In [49]:
%%time

train_loader = torch.utils.data.DataLoader(dataset=train_dataset,    
                                           batch_size=16,
                                           shuffle=True)
batch = next(iter(train_loader))
print('X train batch, shape: {}, data type: {}, Memory usage: {} bytes'
      .format(batch[0].shape, batch[0].dtype, batch[0].element_size()*batch[0].nelement()))
print('Y train batch, shape: {}, data type: {}, Memory usage: {} bytes'
      .format(batch[1].shape, batch[1].dtype, batch[1].element_size()*batch[1].nelement()))

imgs, targets = batch
imgs, targets = mixup_data(imgs, targets, num_classes=1000, alpha=2)        ## Transformation Mixup


for i in range(4):
    img = imgs[i].numpy().transpose((1,2,0))
    plt.imshow(img)
    plt.axis('off')
    plt.show()
    print(f'target : {torch.max(targets, dim=1)[1][i]}, lambda : {torch.max(targets, dim=1)[0][i]}')
X train batch, shape: torch.Size([16, 3, 176, 176]), data type: torch.float32, Memory usage: 5947392 bytes
Y train batch, shape: torch.Size([16]), data type: torch.int64, Memory usage: 128 bytes
target : 714, lambda : 0.5794603824615479
target : 792, lambda : 0.5220049619674683
target : 866, lambda : 0.7600038647651672
target : 387, lambda : 0.7419617176055908
CPU times: user 6.82 s, sys: 175 ms, total: 6.99 s
Wall time: 7.37 s

Paramètre alpha pour la beta distribution

Dans le script mixup.py, la variable lambda (lam) correspond à la proportion de la première image par rapport à la deuxième image. Elle est choisie aléatoirement suivant une distribution bêta définie sur [0, 1].

Le paramètre alpha agit sur la forme de la distribution bêta. alpha = 1 correspond à une distribution uniforme, alpha < 1 favorise un tirage au sort de valeurs proches des bornes 0. ou 1., et alpha > 1 favorise un tirage au sort de valeurs proches du centre 0.5.

In [50]:
for alpha in [0.5, 1., 2.]:
    plt.hist(np.random.beta(alpha, alpha, 1000000), bins=50, density=True, histtype='step')
    plt.title(f'alpha={alpha}')
    plt.show()

Transformation Mixup sur CPU¶

TODO : dans le script dlojz.py :

  • Importer la transformation Mixup
from mixup.mixup import mixup_data
  • Rajouter la transformation MixUp dans la boucle d'apprentissage avant d'envoyer le batch d'images et de labels au GPU.
# distribution of images and labels to all GPUs                                
    images, labels = mixup_data(images, labels, num_classes=1000, alpha=2.)
    images = images.to(gpu, non_blocking=True)
    labels = labels.to(gpu, non_blocking=True)
  • Dans le calcul des métriques à la fin de la boucle d'apprentissage, étant donné que les labels ne sont plus des id de classes mais des vecteurs de type one hot encoded, il faut ajouter la ligne suivante pour calculer les valeurs maximales des vecteurs :
# Metric mesurement
    _, predicted = torch.max(outputs.data, 1)
    labels = torch.argmax(labels, dim=1)     ### line to add for Mixup and Cutmix
    accuracy = (predicted == labels).sum() / labels.size(0)
In [51]:
command = f'dlojz.py -b {bs_optim} --image-size {image_size} --test'
command
Out[51]:
'dlojz.py -b 512 --image-size 176 --test'

Soumission du job. Attention vous sollicitez les noeuds de calcul à ce moment-là.

Pour soumettre le job, veuillez basculer la cellule suivante du mode Raw NBConvert au mode Code.

In [52]:
command = f'dlojz.py -b {bs_optim} --image-size {image_size} --test'
n_gpu = 1
jobid = gpu_jobs_submitter(command, n_gpu, MODULE, name=name,
                    account=account, time_max='00:10:00', constraint='v100-32g')
print(f'jobid = {jobid}')
batch job 0: 1 GPUs distributed on 1 nodes with 1 tasks / 1 gpus per node and 10 cpus per task
Submitted batch job 248278
jobid = ['248278']

Copier-coller la sortie jobid = ['xxxxx'] dans la cellule suivante.

Puis, rebasculer la cellule précédente en mode Raw NBConvert, afin d'éviter de relancer un job par erreur.

In [53]:
#jobid = ['1910208']
In [54]:
display_slurm_queue(name)
             JOBID PARTITION     NAME     USER ST       TIME  NODES NODELIST(REASON)
            248278   gpu_p13   pseudo  cfor132  R       1:09      1 r6i3n0

 Done!
In [55]:
controle_technique(jobid)
Train throughput: 924.57 images/second
GPU throughput: 1792.35 images/second
epoch time: 1386.09 seconds
training time estimation for 90 epochs (with validations): 36.76 hours
-----------
training step time average (fwd/bkwd on GPU): 0.285658 sec (8.2%/94.4%) +/- 0.045335
loading step time average (CPU to GPU): 0.268114 sec +/- 0.032765
-----------
ELIGIBLE to run 28 epochs
In [56]:
turbo_profiler(jobid)
>>> Turbo Profiler >>> Training complete in 46.607843 s

Transformation Mixup sur GPU¶

TODO : dans le script dlojz.py :

  • Appliquer la transformation MixUp dans la boucle d'apprentissage après avoir envoyé le batch d'images et de labels au GPU.
# distribution of images and labels to all GPUs                                
    #images, labels = mixup_data(images, labels, num_classes=1000, alpha=2.) ## ligne déplacée
    images = images.to(gpu, non_blocking=args.non_blocking)
    labels = labels.to(gpu, non_blocking=args.non_blocking)
    images, labels = mixup_data(images, labels, num_classes=1000, alpha=2., device=gpu)

TODO : dans le script mixup/mixup.py :

  • Ajouter le paramètre device=device à chaque fois que l'on crée un nouveau Tensor pour qu'il soit stocké en mémoire au bon emplacement (CPU ou GPU).

Soumission du job. Attention vous sollicitez les noeuds de calcul à ce moment-là.

Pour soumettre le job, veuillez basculer la cellule suivante du mode Raw NBConvert au mode Code.

In [57]:
command = f'dlojz.py -b {bs_optim} --image-size {image_size} --test'
n_gpu = 1
jobid = gpu_jobs_submitter(command, n_gpu, MODULE, name=name,
                    account=account, time_max='00:10:00', constraint='v100-32g')
print(f'jobid = {jobid}')
batch job 0: 1 GPUs distributed on 1 nodes with 1 tasks / 1 gpus per node and 10 cpus per task
Submitted batch job 248300
jobid = ['248300']

Copier-coller la sortie jobid = ['xxxxx'] dans la cellule suivante.

Puis, rebasculer la cellule précédente en mode Raw NBConvert, afin d'éviter de relancer un job par erreur.

In [58]:
#jobid = ['1910460']
In [59]:
display_slurm_queue(name)
             JOBID PARTITION     NAME     USER ST       TIME  NODES NODELIST(REASON)
            248300   gpu_p13   pseudo  cfor132  R       0:57      1 r6i3n0

 Done!
In [60]:
controle_technique(jobid)
Train throughput: 1661.86 images/second
GPU throughput: 1788.91 images/second
epoch time: 771.15 seconds
training time estimation for 90 epochs (with validations): 21.76 hours
-----------
training step time average (fwd/bkwd on GPU): 0.286208 sec (7.7%/94.9%) +/- 0.030817
loading step time average (CPU to GPU): 0.021881 sec +/- 0.009838
-----------
ELIGIBLE to run 41 epochs
In [61]:
turbo_profiler(jobid)
>>> Turbo Profiler >>> Training complete in 35.704246 s

Commentaires


TP3_2_2 : Cutmix¶

Le but de ce TP est d'ajouter la transformation CutMix dans la liste des transformations pour la Data Augmentation et de mesurer grâce au profiler ce que cela implique pour le DataLoader.

La transformation CutMix n'est pas disponible dans torchvision, le script est disponible dans le répertoire cutmix/. On notera que cette transformation impacte à la fois l'image et le label.

On choisira, comme cela est fait habituellement, de mixer 2 images présentes dans le batch généré par le dataloader. Donc cette transformation sera faite dans la boucle d'apprentissage après génération du batch et donc après toutes autres transformations liées à la Data Augmentation.

Dans le script cutmix.py, la variable lambda (lam) correspond à la proportion de la première image par rapport à la deuxième image. Elle est choisie aléatoirement suivant une distribution uniforme définie sur [0, 1].

In [62]:
import os
import torchvision
import torchvision.transforms as transforms
import torchvision.models as models
import torch
import numpy as np
import matplotlib.pyplot as plt

transform = transforms.Compose([ 
        transforms.RandomResizedCrop(176),  # Random resize - Data Augmentation
        transforms.RandomHorizontalFlip(),  # Horizontal Flip - Data Augmentation
        transforms.ToTensor()               # convert the PIL Image to a tensor
        ])
    
    
train_dataset = torchvision.datasets.ImageNet(root=os.environ['ALL_CCFRSCRATCH']+'/imagenet',
                                                  transform=transform)
train_dataset
Out[62]:
Dataset ImageNet
    Number of datapoints: 1281167
    Root location: /gpfsscratch/idris/for/commun/imagenet
    Split: train
    StandardTransform
Transform: Compose(
               RandomResizedCrop(size=(176, 176), scale=(0.08, 1.0), ratio=(0.75, 1.3333), interpolation=bilinear)
               RandomHorizontalFlip(p=0.5)
               ToTensor()
           )
In [63]:
from cutmix.cutmix import cutmix_data
In [64]:
%%time

train_loader = torch.utils.data.DataLoader(dataset=train_dataset,    
                                           batch_size=16,
                                           shuffle=True)
batch = next(iter(train_loader))
print('X train batch, shape: {}, data type: {}, Memory usage: {} bytes'
      .format(batch[0].shape, batch[0].dtype, batch[0].element_size()*batch[0].nelement()))
print('Y train batch, shape: {}, data type: {}, Memory usage: {} bytes'
      .format(batch[1].shape, batch[1].dtype, batch[1].element_size()*batch[1].nelement()))

imgs, targets = batch
imgs, targets = cutmix_data(imgs, targets, num_classes=1000)


for i in range(4):
    img = imgs[i].numpy().transpose((1,2,0))
    plt.imshow(img)
    plt.axis('off')
    plt.show()
    print(f'target : {torch.max(targets, dim=1)[1][i]}, lambda : {torch.max(targets, dim=1)[0][i]}')
X train batch, shape: torch.Size([16, 3, 176, 176]), data type: torch.float32, Memory usage: 5947392 bytes
Y train batch, shape: torch.Size([16]), data type: torch.int64, Memory usage: 128 bytes
target : 433, lambda : 0.9442148804664612
target : 400, lambda : 1.0
target : 718, lambda : 0.5805785059928894
target : 737, lambda : 1.0
CPU times: user 6.92 s, sys: 225 ms, total: 7.15 s
Wall time: 7.55 s

Transformation CutMix sur GPU¶

TODO : dans le script dlojz.py :

  • Importer la transformation CutMix
from cutmix.cutmix import cutmix_data
  • Rajouter la transformation CutMix dans la boucle d'apprentissage après avoir envoyé le batch d'images et de labels au GPU.
# distribution of images and labels to all GPUs
    images = images.to(gpu, non_blocking=args.non_blocking)
    labels = labels.to(gpu, non_blocking=args.non_blocking)
    images, labels = cutmix_data(images, labels, num_classes=1000, device=gpu)

Soumission du job. Attention vous sollicitez les noeuds de calcul à ce moment-là.

Pour soumettre le job, veuillez basculer la cellule suivante du mode Raw NBConvert au mode Code.

In [65]:
command = f'dlojz.py -b {bs_optim} --image-size {image_size} --test'
n_gpu = 1
jobid = gpu_jobs_submitter(command, n_gpu, MODULE, name=name,
                    account=account, time_max='00:10:00', constraint='v100-32g')
print(f'jobid = {jobid}')
batch job 0: 1 GPUs distributed on 1 nodes with 1 tasks / 1 gpus per node and 10 cpus per task
Submitted batch job 248327
jobid = ['248327']

Copier-coller la sortie jobid = ['xxxxx'] dans la cellule suivante.

Puis, rebasculer la cellule précédente en mode Raw NBConvert, afin d'éviter de relancer un job par erreur.

In [66]:
#jobid = ['226430']
In [67]:
display_slurm_queue(name)
             JOBID PARTITION     NAME     USER ST       TIME  NODES NODELIST(REASON)
            248327   gpu_p13   pseudo  cfor132  R       0:55      1 r6i3n0

 Done!
In [68]:
controle_technique(jobid)
Train throughput: 1341.21 images/second
GPU throughput: 1789.47 images/second
epoch time: 955.51 seconds
training time estimation for 90 epochs (with validations): 25.98 hours
-----------
training step time average (fwd/bkwd on GPU): 0.286119 sec (8.3%/95.3%) +/- 0.047890
loading step time average (CPU to GPU): 0.095627 sec +/- 0.018096
-----------
ELIGIBLE to run 36 epochs
In [69]:
turbo_profiler(jobid)
>>> Turbo Profiler >>> Training complete in 37.928266 s

Optimisation de la transformation CutMix¶

Le code précédent utilise une boucle for qui empêche de distribuer la transformation sur les cores du GPU. Chaque image dans le batch est traitée de manière séquentielle.

Le but de cette partie est d'optimiser le code de CutMix en générant du calcul matriciel adapté à une parallélisation sur GPU. Il s'agira de manipuler des tenseurs de tailles proportionnelles au batch size et d'utiliser des fonctions d'algèbre linéaire pour aboutir au même résultat numérique tout en accélérant le calcul.

En d'autres termes, au lieu de constituer un masque par image, nous allons directement créer un batch de masques pour tout un batch d'images.

In [ ]:
 

Création d'un batch de masques

Dans un premier temps, pour comprendre la procédure, nous travaillerons avec un batch de 3 images de taille 32x32.

In [70]:
import torch
import numpy as np
import matplotlib.pyplot as plt
batch_size = 3
W = 32
H = 32

En entrée, on connait les coordonnées des coins du cadre à découper pour chaque image du batch (voir illustration ci-dessous).

In [71]:
# coordonnee min dans la largeur pour chaque image du batch
x1 = torch.Tensor([10, 5, 23]).long()
# coordonne max dans la largeur pour chaque image du batch
x2 =  torch.Tensor([20, 25, 31]).long()
# coordonnee min dans la hauteur pour chaque image du batch
y1 =  torch.Tensor([5, 10, 0]).long()
# coordonne max dans la hauteur pour chaque image du batch
y2 =  torch.Tensor([10, 22, 20]).long()

cutmix_opt

1. Création des vecteurs ligne "largeur" w_int et des vecteurs colonne "hauteur" h_int pour tout le batch d'images

In [72]:
# initialisation à zéro
w_int = torch.zeros(batch_size,1,W) # vecteurs ligne
h_int = torch.zeros(batch_size,H,1) # vecteurs colonne

On initialise les éléments correspondant aux coordonnées minimales (x1 et y1) à 1.
On initialise les éléments correspondant aux coordonnées maximales (x2 et y2) à -1.
Par la suite, les intervalles [x1,x2] et [y1,y2] seront remplis de 1 en demandant à remplir chaque vecteur avec la somme cumulée de ses éléments.

In [73]:
batch_idx = torch.arange(0,batch_size)
# initialisation des indices correspondant aux coord min x1 et y1 à 1
w_int[batch_idx,0,x1] = 1.
h_int[batch_idx,y1,0] = 1.

# initialisation des indices correspondant aux coord max x2 et y2 à -1
w_int[batch_idx,0,x2] = -1.
h_int[batch_idx,y2,0] = -1.
In [74]:
# visualisation des vecteurs ligne "largeur" w_int
for wx in w_int:
    plt.imshow(wx)
    plt.clim(-1,1)
    plt.colorbar(ticks=np.arange(-1,2))
    plt.show()
In [75]:
# visualisation des vecteurs colonne "hauteur"
for hx in h_int:
    plt.imshow(hx)
    plt.clim(-1,1)
    plt.colorbar(ticks=np.arange(-1,2))
    plt.show()

Pour créer nos vecteurs w_int et h_int, on remplit chaque intervalle [x1,x2] et [y1,y2] de 1 en utilisant la fonction torch.cumsum pour cumuler les valeurs des éléments des vecteurs.

In [76]:
# torch.cumsum(input, dim, *, dtype=None, out=None) → Tensor
# Returns the cumulative sum of elements of input in the dimension dim.
# Parameters
#        input (Tensor) – the input tensor.
#        dim (int) – the dimension to do the operation over

w_int = torch.cumsum(w_int, dim=2) # vecteurs ligne
h_int = torch.cumsum(h_int, dim=1) # vecteurs colonne
In [77]:
# visualisation des vecteurs masques "largeur"
for wx in w_int:
    plt.imshow(wx)
    plt.clim(-1,1)
    plt.colorbar(ticks=np.arange(-1,2))
    plt.show()
In [78]:
# visualisation des vecteurs masques "hauteur"
for hx in h_int:
    plt.imshow(hx)
    plt.clim(-1,1)
    plt.colorbar(ticks=np.arange(-1,2))
    plt.show()

2. Créations du batch de masques intérieurs et extérieurs

  • Multiplication des vecteurs h_int et w_int pour obtenir les masques intérieurs pour chaque image du batch.
In [79]:
# multiplication des vecteurs colonne "hauteur" h_int par les vecteurs ligne "largeur" w_int
mask_int = h_int*w_int
In [80]:
# visualisation des masques intérieurs pour chaque image du batch
for m in mask_int:
    plt.imshow(m)
    plt.clim(-1,1)
    plt.colorbar(ticks=np.arange(-1,2))
    plt.show()
  • Puis, création des masques extérieurs à partir des masques intérieurs.
In [81]:
# les masques extérieurs sont les complémentaires des masques intérieurs
mask_ext = mask_int * (-1) + 1
In [82]:
# visualisation des masques extérieurs
for m in mask_ext:
    plt.imshow(m)
    plt.clim(-1,1)
    plt.colorbar(ticks=np.arange(-1,2))
    plt.show()

Implémentation de la fonction de création d'un batch de masques

Maintenant, l'idée est d'implémenter ce qui a été fait dans les cellules précédentes dans une fontion générique, en ajoutant un choix sur le device d'exécution.

TODO : implémenter la fonction de création des masques dans la cellule suivante. Les entrées de la fonction sont :

  • les coordonnées x1, x2, y1, y2,
  • le batch_size,
  • la largeurW de l'image,
  • la hauteur H de l'image,
  • le device de calcul.

Important : Pour les images RGB (channel de 3), il faut rajouter une dimension en deuxième position dans les masques finaux :

# rajouter une dimension en 2e position pour pouvoir traiter des images RGB
    mask_int = mask_int.unsqueeze(1) 
    mask_ext = mask_ext.unsqueeze(1)

Attention : Ne pas oublier le paramètre device=device à chaque création d'un nouveau Tensor. Par exemple pour :

w_int = torch.zeros(batch_size,1,W,device=device)
In [83]:
def cut_mask(x1, x2, y1, y2, batch_size, W, H, device=None):
    
    mask_ext, mask_int = None, None
    
    ### TODO
    # initialisation à zéro
    w_int = torch.zeros(batch_size,1,W,device=device) # vecteurs ligne
    h_int = torch.zeros(batch_size,H,1,device=device) # vecteurs colonne
    
    batch_idx = torch.arange(0,batch_size,device=device)
    # initialisation des indices correspondant aux coord min x1 et y1 à 1
    w_int[batch_idx,0,x1] = 1.
    h_int[batch_idx,y1,0] = 1.

    # initialisation des indices correspondant aux coord max x2 et y2 à -1
    w_int[batch_idx,0,x2] = -1.
    h_int[batch_idx,y2,0] = -1.
    
    w_int = torch.cumsum(w_int, dim=2) # vecteurs ligne
    h_int = torch.cumsum(h_int, dim=1) # vecteurs colonne

    # multiplication des vecteurs colonne "hauteur" h_int par les vecteurs ligne "largeur" w_int
    mask_int = h_int*w_int
    
    # les masques extérieurs sont les complémentaires des masques intérieurs
    mask_ext = mask_int * (-1) + 1
    
    # rajouter une dimension pour les images RGB
    mask_int = mask_int.unsqueeze(1)
    mask_ext = mask_ext.unsqueeze(1) 
    
    return mask_ext, mask_int

Test de la fonction implémentée¶

In [84]:
%%time

train_loader = torch.utils.data.DataLoader(dataset=train_dataset,    
                                           batch_size=16,
                                           shuffle=True)
batch = next(iter(train_loader))
print('X train batch, shape: {}, data type: {}, Memory usage: {} bytes'
      .format(batch[0].shape, batch[0].dtype, batch[0].element_size()*batch[0].nelement()))
print('Y train batch, shape: {}, data type: {}, Memory usage: {} bytes'
      .format(batch[1].shape, batch[1].dtype, batch[1].element_size()*batch[1].nelement()))

imgs, targets = batch
X train batch, shape: torch.Size([16, 3, 176, 176]), data type: torch.float32, Memory usage: 5947392 bytes
Y train batch, shape: torch.Size([16]), data type: torch.int64, Memory usage: 128 bytes
CPU times: user 4.25 s, sys: 20.7 ms, total: 4.27 s
Wall time: 4.32 s
In [85]:
batch_size = 16
W = 176
H = 176
In [86]:
lam = torch.rand(batch_size)
s_index = torch.randperm(batch_size)      # Shuffle index
rand_x = torch.randint(W, (batch_size,))
rand_y = torch.randint(H, (batch_size,))
cut_rat = torch.sqrt(1. - lam) ## cut ratio according to the random lambda

x1 = torch.clip(rand_x - rand_x / 2, min=0).long()
x2 = torch.clip(rand_x + rand_x / 2, max=W-1).long()
y1 = torch.clip(rand_y - rand_y / 2, min=0).long()
y2 = torch.clip(rand_y + rand_y / 2, max=H-1).long()

mask_ext, mask_int = cut_mask(x1, x2, y1, y2, batch_size, W, H)
In [87]:
# vérifier si le masque et l'image ont le même nombre de dimensions
try:
    assert imgs.dim() == mask_int.dim()
    print('OK!')
except:
    print(f'Mismatch: \n dim imgs = {imgs.dim()} \n dim mask = {mask_int.dim()} ')
OK!
In [88]:
imgs = mask_ext * imgs + mask_int * imgs[s_index, :]
In [89]:
for i in range(4):
    img = imgs[i].numpy().transpose((1,2,0))
    plt.imshow(img)
    plt.axis('off')
    plt.show()

Puis si le résultat est satisfaisant, intégrer la fonction dans le code cutmix/cutmix.py.

TODO : dans le script cutmix/cutmix.py, ajouter la fonction cut_mask définie dans la cellule plus haut.

Soumission du job. Attention vous sollicitez les noeuds de calcul à ce moment-là.

Pour soumettre le job, veuillez basculer la cellule suivante du mode Raw NBConvert au mode Code.

In [90]:
command = f'dlojz.py -b {bs_optim} --image-size {image_size} --test'
n_gpu = 1
jobid = gpu_jobs_submitter(command, n_gpu, MODULE, name=name,
                    account=account, time_max='00:10:00', constraint='v100-32g')
print(f'jobid = {jobid}')
batch job 0: 1 GPUs distributed on 1 nodes with 1 tasks / 1 gpus per node and 10 cpus per task
Submitted batch job 248337
jobid = ['248337']

Copier-coller la sortie jobid = ['xxxxx'] dans la cellule suivante.

Puis, rebasculer la cellule précédente en mode Raw NBConvert, afin d'éviter de relancer un job par erreur.

In [91]:
#jobid = ['256363']
In [92]:
display_slurm_queue(name)
             JOBID PARTITION     NAME     USER ST       TIME  NODES NODELIST(REASON)
            248337   gpu_p13   pseudo  cfor132  R       0:56      1 r6i3n0

 Done!
In [93]:
controle_technique(jobid)
Train throughput: 1669.66 images/second
GPU throughput: 1790.34 images/second
epoch time: 767.54 seconds
training time estimation for 90 epochs (with validations): 21.75 hours
-----------
training step time average (fwd/bkwd on GPU): 0.285979 sec (6.9%/96.2%) +/- 0.030070
loading step time average (CPU to GPU): 0.020670 sec +/- 0.006818
-----------
ELIGIBLE to run 41 epochs
In [94]:
turbo_profiler(jobid)
>>> Turbo Profiler >>> Training complete in 35.818331 s
In [ ]: